package org.rabbitmq.mq02.mqServer.datacore;

import lombok.Data;
import org.rabbitmq.mq02.common.BinaryTool;
import org.rabbitmq.mq02.common.MqException;
import org.rabbitmq.mq02.mqServer.core.MSGQueue;
import org.rabbitmq.mq02.mqServer.core.Message;

import java.io.*;
import java.util.LinkedList;
import java.util.List;
import java.util.Scanner;

/**
 * 用于管理消息在文件中的存储
 * 该类中药完成的事情:1.创建数据文件,统计文件的目录,文件
 *               2.读写统计文件
 *               3.读写数据文件
 *               4.读取文件中所有的有效消息
 *               5.对文件中的消息进行垃圾回收
 */
public class MessageFileManager {
    /**
     * 创建一个内部类,表示消息的统计文件类
     */
    public static class Stat {
        //消息总数
        public int totalCount ;
        //有效消息数
        public int validCount ;
    }
    //这里目前没有要初始化的
    public void init(){}

//获取队列文件目录名:
    public String getQueueDir(String queueName){
        return "./data/"+queueName;
    }
    //获取消息 数据文件 路径名
    public String getDataFilePath(String queueName){
        return getQueueDir(queueName)+"/queue_data.txt";
    }
    //获取消息 统计文件 路径名
    public String getStatFilePath(String queueName){
        return getQueueDir(queueName)+"/queue_stat.txt";
    }
    //创建队列文件
    public void createQueueFile(String queueName) throws MqException, IOException {
        File file = new File(getQueueDir(queueName));
        //当目录不存在时,创建目录
        if (!file.exists()) {
            boolean ok = file.mkdirs();
            if (!ok) {
                throw new MqException("[MessageFileManager] 创建目录失败!");
            }
        }
        //创建文件
        File dataFile = new File(getDataFilePath(queueName));
        if (!dataFile.exists()) {
            //数据文件不存在时,创建数据文件:
            boolean ok = dataFile.createNewFile();
            if (!ok) {
                throw new MqException("[MessageFileManager] 创建数据文件失败!");
            }
        }
        File statFile = new File(getStatFilePath(queueName));
        if (!statFile.exists()) {
            boolean ok = statFile.createNewFile();
            if (!ok) {
                throw new MqException("[MessageFileManager] 创建统计文件失败!");
            }
        }
        //初始化统计文件数据
        Stat stat = new Stat();
        stat.totalCount = 0;
        stat.validCount = 0;
        writeStat(queueName,stat);
        System.out.println("[MessageFileManager] 创建目录及文件成功!");
    }

    //删除指定队列的目录和文件
    //当队列被删除后,对应的文件和目录也就要被删除了
    public void destoryQueueFile(String queueName) throws IOException {
        //1.删除数据文件
        File dataFile = new File(getDataFilePath(queueName));
        boolean ok1 = dataFile.delete();
        //2.删除统计文件
        File statFile = new File(getStatFilePath(queueName));
        boolean ok2 = statFile.delete();
        //3.删除总目录
        File baseDir = new File(getQueueDir(queueName));
        boolean ok3 = baseDir.delete();
        if (!ok1 || !ok2 || !ok3) {
            throw new IOException("删除队列和文件失败! baseDir: " + baseDir.getAbsolutePath());
        }
    }

    //从统计文件中读取消息
    private Stat readStat(String queueName){
        Stat stat = new Stat();
        try (FileInputStream fileInputStream = new FileInputStream(getStatFilePath(queueName))){
            //通过Scanner来读取文件中的数据
            Scanner scan = new Scanner(fileInputStream);
            stat.totalCount = scan.nextInt();
            stat.validCount = scan.nextInt();
            return stat;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    //写消息到统计文件中
    private void writeStat(String queueName,Stat stat){
        //注意:此处打开文件,默认是清空文件的,可以通过设置第二个参数为true:拼接文件形式打开文件,不会清空数据
        try(FileOutputStream outputStream = new FileOutputStream(getStatFilePath(queueName))){
            PrintWriter printWriter = new PrintWriter(outputStream);
            printWriter.write(stat.totalCount+"\t"+stat.validCount);
            //注意:要刷新缓冲区,将写入的数据从缓冲区都放到磁盘中
            printWriter.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    //向数据文件中发送消息
    //1.判断文件是否存在
    //2.将消息序列化
    //3.写入数据文件
    //4.更新统计文件数据
    //考虑线程安全问题:
    //当多个线程同时发送消息时,可能会发送线程安全问题,这里以队列为维度加锁,因此要传入参数队列对象
    public void sendMessage(MSGQueue queue, Message message) throws MqException, IOException {
        //1.判断文件是否存在:
        boolean ok = checkFileExists(queue.getName());
        if(!ok){
            throw new MqException("[MessageFileManager] 数据文件不存在,发送消息到数据文件失败 queueName: "+queue.getName());
        }
        synchronized(queue){
            //2.将消息先进行序列化:
            byte[] messageByte = BinaryTool.toByte(message);
            //3.写消息到文件中:
            File file = new File(getDataFilePath(queue.getName()));
            //设置 消息在文件中的初始位置属性:
            message.setOffsetBeg(file.length()+4);
            message.setOffsetEnd(file.length()+4+messageByte.length);
            //这里打开文件要以追加的方式打开,不能清空文件中的内容
            try(FileOutputStream outputStream = new FileOutputStream(file,true);
                DataOutputStream dataOutputStream = new DataOutputStream(outputStream)){
                //先写前4个字节:记录消息的长度,
                //注意,这里规定的就是用4字节的长度,不能单使用write写入,这样还是只写入1个字节
                dataOutputStream.writeInt(messageByte.length);
                dataOutputStream.write(messageByte);
            }
            //更新统计文件数据
            Stat stat = readStat(queue.getName());
            stat.totalCount+=1;
            stat.validCount+=1;
            writeStat(queue.getName(),stat);
        }
    }

    private boolean checkFileExists(String queueName) throws MqException {
        File baseFile = new File(getQueueDir(queueName));
        if(!baseFile.exists()){
            System.out.println("[MessageFileManager] 队列对应的目录不存在 queueName:"+queueName);
            return false;
        }
        File dataFile = new File(getDataFilePath(queueName));
        if(!dataFile.exists()){
            System.out.println("[MessageFileManager] 队列对应的数据文件不存在 queueName:"+queueName);
            return false;
        }
        File statFile = new File(getStatFilePath(queueName));
        if(!statFile.exists()){
            System.out.println("[MessageFileManager] 队列对应的统计文件不存在 queueName:"+queueName);
            return false;
        }
        return true;
    }

    //将一条消息从文件中删除(逻辑删除,将isValid置为无效:0x0
    //1.先读取到消息
    //2.将消息的isValid设为无效
    //3.再重新写入到文件原来位置
    //4.修改配置文件数据
    //删除文件也存在线程安全问题,要以队列为维度进行上锁
    public void deleteMessageFromFile(MSGQueue queue,Message message){
        synchronized(queue){
            //要找消息的位置,采用随机读取文件方法,且以可读可写的方式打开文件
            try(RandomAccessFile randomAccessFile = new RandomAccessFile(getDataFilePath(queue.getName()),"rw")){
                //offsetBeg记录的是消息数据开始的位置,不包含前4个统计数据长度
                int len = (int)(message.getOffsetEnd() - message.getOffsetBeg());
                byte[] data = new byte[len];
                //定位光标到消息的起始位置:
                randomAccessFile.seek(message.getOffsetBeg());
                //从消息的起始位置读取文件
                randomAccessFile.read(data);
                //将数据反序列化:
                 Message newMessage = (Message) BinaryTool.fromBytes(data);
                 //修改为无效消息
                newMessage.setIsVaild((byte)0x0);
                //再将消息写入到文件中:
                //先进行序列化消息
                byte[] payload = BinaryTool.toByte(newMessage);
                //要写到原来位置,在读取的时候,光标会变化,要重新设置光标,
                randomAccessFile.seek(message.getOffsetBeg());
                randomAccessFile.write(payload);

                //更新统计文件数据
                Stat stat = readStat(queue.getName());
                if(stat.validCount>0){
                    stat.validCount-=1;
                }
                writeStat(queue.getName(),stat);
            } catch (IOException | ClassNotFoundException e) {
                throw new RuntimeException(e);
            }
        }
    }

    //获取文件中的所有有效数据,该方法用户服务器宕机重启时,恢复数据
    public List<Message> loadAllMessage(String queueName) throws IOException, MqException, ClassNotFoundException {
        LinkedList<Message> messages = new LinkedList<>();
        try(FileInputStream fileInputStream = new FileInputStream(getDataFilePath(queueName));
            DataInputStream dataInputStream = new DataInputStream(fileInputStream)){
            long currentOffset = 0;
            while(true){
                int len = dataInputStream.readInt();
                //读取数据
                byte[] data = new byte[len];
                int n = dataInputStream.read(data);
                if(n!=len){
                    throw new MqException("[MessageFileManager] 读取消息格式有误! queueName:"+queueName);
                }
                //进行反序列化
                Message message = (Message) BinaryTool.fromBytes(data);
                byte n1 = message.getIsVaild();
                if(n1==0x0){
                    //为无效消息,直接跳过
                    currentOffset+=(4+len);
                    continue;
                }
                //设置消息的起始位置
                message.setOffsetBeg(currentOffset+4);
                message.setOffsetEnd(currentOffset+4+len);
                currentOffset+=(4+len);
                messages.add(message);
            }
        } catch (EOFException e){
            //这个异常是读取到文件末尾,抛出的异常,属于正常逻辑
            System.out.println("[MessageFileManager] 加载消息完成 !");
        }
        return messages;
    }

    //进行垃圾回收:当统计文件中记录的有效数据数量占总数据数量<50%时,进行垃圾回收
    //垃圾回收:新创建一个文件,将有效消息复制到新文件中,复制完后,将源文件删除,将新文件名改为源文件名
    public void GC(MSGQueue queue) throws IOException, MqException, ClassNotFoundException {
        //创建一个新的文件:
        File newFile = new File(getNewDataFilePath(queue.getName()));
        if(newFile.exists()){
            throw new MqException("[MessageFileManager] 上次GC未执行成功, queueName:"+queue.getName());
        }
        boolean ok = newFile.createNewFile();
        if(!ok){
            throw new MqException("[MessageFileManager] 创建新文件失败! queueName:"+queue.getName());
        }
        //将原来文件中的消息都取出来
        List<Message> messages = loadAllMessage(queue.getName());
        //将消息进行序列化,再存入新文件中
        try(FileOutputStream outputStream = new FileOutputStream(newFile,true);
            DataOutputStream dataOutputStream = new DataOutputStream(outputStream)){
            for(Message m:messages){
                byte[] payload = BinaryTool.toByte(m);
                dataOutputStream.writeInt(payload.length);
                dataOutputStream.write(payload);
            }
        }
        //删除旧文件
        String oldDataFilePath = getDataFilePath(queue.getName());
        File oldFile = new File(oldDataFilePath);
        ok = oldFile.delete();
        if(!ok){
            throw new MqException("[MessageFileManager] 旧文件删除失败! oldFilePath:"+oldFile.getAbsolutePath());
        }
        //更新新文件名:
        ok = newFile.renameTo(oldFile);
        if(!ok){
            throw new MqException("[MessageFileManager] 更新文件名失败 newFilePath"+newFile.getAbsolutePath());
        }
        //更新统计文件数据
        Stat stat = readStat(queue.getName());
        stat.totalCount = messages.size();
        stat.validCount = messages.size();
        writeStat(queue.getName(),stat);
    }
//创建一个新的文件路径:
    private String getNewDataFilePath(String queueName) {
        return getQueueDir(queueName)+"newQueue_data.txt";
    }
//查看当前队列是否需要进行GC扫描
    public boolean checkGC(String queueName){
        Stat stat = readStat(queueName);
        int t1 = stat.validCount;
        int t2 = stat.totalCount;
        if(t2>=2000 && t1*1.0 /t2*1.0<0.5){
            return true;
        }
        return false;
    }


}
